import {
  ExecutionContext,
  createParamDecorator,
  applyDecorators,
  SetMetadata,
} from '@nestjs/common'
import { BoticordAbility } from '../casl/ability.type'
import { IPolicyHandler, SimplePolicyHandler } from '../interfaces/policy-handler.interface'
import { Action } from '@boticord/auth/casl/action.enum'
import { TokenType } from '@boticord/auth/interfaces/token.type'
import {
  FetchingPolicy,
  FetchingPolicyOptions,
  IdentifiableModel,
} from './fetching-policy.decorator'
import { Token } from '../models/token.model'
import { JwtPayloadDto } from '@boticord/auth/dto/jwt-payload.dto'

export const AUTH_ENABLED_KEY = Symbol('AuthEnabled')
export const AUTH_POLICY_KEY = Symbol('AuthPolicy')

export const OffAuth = () => applyDecorators(
  SetMetadata(AUTH_ENABLED_KEY, false),
)

export const CheckPolicies = (handler: new(...args: any[]) => IPolicyHandler) => applyDecorators(
  SetMetadata(AUTH_ENABLED_KEY, true),
  SetMetadata(AUTH_POLICY_KEY, handler),
)

export interface PermissionsDecoratorOptions<T> {
  token?: TokenType
  model?: new(...args: any[]) => IdentifiableModel
  fetch?: FetchingPolicyOptions<T>
}

const checkTokenType = (tokenType: TokenType | undefined, token?: Token) => {
  if (!token) return true

  if (tokenType && tokenType !== 'any') {
    if (tokenType === 'bearer' && !token.isUser()) {
      return false
    } else if (tokenType === 'bot' && token.isUser()) {
      return false
    }
  }

  return true
}

// check user permissions to perform Action on Model and also require token type (default: bearer)
export const PermissionsTo = <T extends IdentifiableModel>(
  action: Action | Action[],
  forModel: 'all' | (new(...args: any[]) => T),
  options: PermissionsDecoratorOptions<T> = { token: 'bearer' },
) => {
  const { token: tokenType } = options
  let handler: new(...args: any[]) => IPolicyHandler

  if (options.fetch && forModel !== 'all') {
    handler = FetchingPolicy(
      forModel,
      (res, ability, context, token) => {
        if (!checkTokenType(tokenType, token)) return false
        return Array.isArray(action) ? action.every(p => ability.can(p, res)) : ability.can(action, res)
      },
      options.fetch,
    )
  } else {
    handler = SimplePolicyHandler(
      (ab, ctx, token) => {
        if (!checkTokenType(tokenType, token)) return false
        return Array.isArray(action) ? action.every(p => ab.can(p, forModel)) : ab.can(action, forModel)
      },
    )
  }

  return applyDecorators(
    SetMetadata(AUTH_ENABLED_KEY, true),
    SetMetadata(AUTH_POLICY_KEY, handler),
  )
}

export const resolveClient = <T>(ctx: ExecutionContext): T => {
  if (ctx.getType() === 'ws') {
    return ctx.switchToWs().getClient<T>()
  } else if (ctx.getType() === 'http') {
    return ctx.switchToHttp().getRequest<T>()
  } else {
    return ctx.switchToRpc().getContext<T>()
  }
}

// extract jwt token from request and return it
export const JwtPayload = createParamDecorator((_: unknown, ctx: ExecutionContext) => {
  const client = resolveClient<{ jwt: JwtPayloadDto }>(ctx)
  return client.jwt
})

// extract request author permissions from the request and return it
export const InjectPermissions = createParamDecorator((_: unknown, ctx: ExecutionContext) => {
  const client = resolveClient<{ perms: number }>(ctx)
  return client.perms
})

export const InjectAbility = createParamDecorator((_: unknown, ctx: ExecutionContext) => {
  const client = resolveClient<{ ability: BoticordAbility }>(ctx)
  return client.ability
})

export const RPC_ENABLED = Symbol('RPC_ENABLED')
export const InternalRpc = () => SetMetadata(RPC_ENABLED, true)
